okinawa

IT勉強メモ

テーブル結合でハマったこと

LEFT OUTER JOINで左側にだけ結合条件つけても意味なし

下記のような左テーブルにしか効かない結合条件しても意味無し。

左側は全て出力されるので。

select * from lefttable L
left outer join righttable R
on L.TENPO = '001' --⇐左テーブルにだけの結合条件

例:テーブル2と3を結合

table2とtable3

・まずは普通の結合

SELECT * FROM Table3 t3
LEFT OUTER JOIN table2 t2
ON t3.tenpo = t2.tenpo
and t3.[group] = t2.[group]

結果

・左側にだけ結合条件つけるSQL

SELECT * FROM Table3 t3
LEFT OUTER JOIN table2 t2
ON t3.tenpo = t2.tenpo
and t3.[group] = t2.[group]
and t3.tenpo = '002' --⇐左側テーブルにだけ結合条件

結果2

左側(テーブル3)は「 t3.tenpo = '002'」以外も全部出力されるので意味なし。

結合条件を複数テーブルにするときは注意

下記はテーブル1・2・3を結合するSQL。1と2は普通に結合。3は1と2両方を結合条件に入れている。

SELECT * FROM Table1 t1
LEFT OUTER JOIN table2 t2
on t1.tenpo = t2.tenpo
and t1.[group] = t2.[group]
and t1.date = t2.date
LEFT OUTER JOIN Table3 t3
on t1.tenpo = t3.tenpo --table1と3
and t1.[group] = t3.[group] --table1と3
and t2.date = t3.date --ここだけtable2と3で結合。テーブル1にはないけどテーブル2にはある日付と結合したいんだよなあという時。

・イメージ
t1.date = t2.date でテーブル1にない日付はテーブル2部分もNULLになっている。
t2.date = t3.date でテーブル1にはないけどテーブル2にはある日付と結合しようとしてもそこはNULL。結合できない。

論理的思考能力には瞬発型と長考型の2つがあるのでは

瞬発と長考

瞬発:ほとんど考えることなくぱっと答えが思い浮かぶ。

長考:じっくりと筋道立てて答えを導き出す。

なんでこんなこと考えたか

仕事してるとえらいスピードで答えを出す人が時々いるので、どうなってるんやと不思議に思うことがあった。

自分はどっちタイプか

たぶん長考型。

昔からRPGやらシミュレーションゲームみたいなじっくり系のゲームが好きだった。

FFシリーズ・カルネージハートギレンの野望タクティクスオウガFFTとか色々やったなあ。どれも面白かった。

逆にアクションゲームは苦手だった。

モンハンとアーマードコアはかなりやったけど。

羽生先生の本に書いてあったこと

瞬発と長考について書いてある本がないか探してみたら、羽生善治先生の本に似たことが書いてあった。

論理的思考の蓄積が、思考スピードを速め、直感を導いてくれる。計算機の言葉でいえば、毎回決まったファンクションが実行されているうちにハードウェア化するようなものだ。それまでは毎回発火していた脳のニューロンが、その発火の仕方がいつも同じなので、そこに結合が生まれ、一種の学習が行われたということではないか。(中略)つまり直感とは論理的思考能力が瞬時に行われるようなものだというのだ。 引用元:直感力 羽生善治

なるほどー。 反復によって一瞬で答えを導き出せるようになるのね。

スポーツも反復練習によって反射的に理想の動作をできるようになるまでやるし、思考でも同じことが起こるのかあ。

もうひとつ引用。

棋士は、若いときには計算する力、記憶力、反射神経の良さを全面に出して対局をするが、年齢を重ねるにつれ少しずつ直感、大局観にシフトしていくのが普通の流れだ。直感や大局観は、一秒にも満たないような短い時間であっても自分の経験則と照らし合わせて使うものなので、ある程度の実地経験を積んでから出ないと使えないと思っている。引用元:直感力 羽生善治

ということで、直感力は先天的なものではなく、むしろ後天的に育っていくものらしい。

ちょっと意外だった。直感力が先天的なもので、じっくり思考が後天的かなあと思っていたので。

私も頑張って直感力を磨こう!

参考

LeetCodeその4 Top K Frequent

int配列の中から出現数が最も多い要素をk個取り出す問題

自分の解答

   /** 自分の解答 Accepted!
    * Map<数値, 出現数>に一旦格納してから
    * Map<出現数, 数値リスト>に格納し直す
    * 出現数でソートして高出現数順に取り出す。
    *  **/
    public int[] topKFrequent(int[] nums, int k) {

        Map<Integer, Integer> map = new HashMap<>();//Map<数値, 出現数>
        Map<Integer, List<Integer>> map2 = new HashMap<>();// Map<出現数, 数値リスト>

        for (int num : nums) {
            // 存在判定
            if (map.containsKey(num)) {
                //既に存在すれば出現数に+1
                map.put(num, map.get(num) + 1);
            } else {
                //なければ新規追加
                map.put(num, 1);
            }
        }

        // 出現数⇔数値を入れ替え
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            List<Integer> list = new ArrayList<>();

            if (map2.containsKey(entry.getValue())) {

                // 出現回数が同じなら同じ要素に入れる
                list = map2.get(entry.getValue());
                list.add(entry.getKey());
                map2.put(entry.getValue(), list);
            } else {

                // 出現回数が違うものは新規追加
                list.add(entry.getKey());
                map2.put(entry.getValue(), list);
            }
        }

        /** 出現数でソートして上位K個分を抽出 **/
        // 出現数をint配列に格納
        int count = 0;
        int[] sort = new int[map2.size()];
        for (Map.Entry<Integer, List<Integer>> entry : map2.entrySet()) {
            sort[count] = entry.getKey();
            count++;
        }

        Arrays.sort(sort);

        // 出現数順にk個取り出す
        int count2 = 0;
        int[] answer = new int[k];
        for (int i = 0; i < sort.length; i++) {
            // 後ろから順(高頻度順に取り出す)
            List<Integer> list2 = map2.get(sort[sort.length - 1 - i]);

            //リストから答え(数値)を取り出す
            for (int num2 : list2) {
                if (count2 >= answer.length) {
                    break;
                }
                answer[count2] = num2;
                count2++;
            }
        }
        return answer;
    }

自分の解答2

答えをListに格納するバージョン。

出現数順にソートする処理をなくせた。

   public int[] topKFrequent2(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();//Map<数値, 出現数>
        int[] answer = new int[k];
    
        int max = 1;
        for (int num : nums) {
            // 存在判定
            if (map.containsKey(num)) {
                //既に存在すれば出現数に+1
                int appearTime = map.get(num) + 1;
                map.put(num, appearTime);
                if(max < appearTime) {
                    max = appearTime;
                }
            } else {
                //なければ新規追加
                map.put(num, 1);
            }
        }
                //  [出現回数][数値]の2次元配列
        List<List<Integer>> list = Stream.generate(() -> new ArrayList<Integer>())
                .limit(max)
                .collect(Collectors.toList());

        // 添字番号=出現回数でListに数値を格納
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            list.get(entry.getValue()-1).add(entry.getKey());
        }
        
        int count2 = 0;
        for(int i = 0; i < nums.length; i++) {
            List<Integer> list2 = list.get(list.size()-1 -i);
            if(list2 == null) {
                continue;
            }
            for(int ans : list2) {
                answer[count2] = ans;
                count2++;
                
                if(count2 >= k) {
                    return answer;
                }
            }

        }
        return null;
    }
}

模範解答

github.com

学んだこと

計算の途中結果を格納する場所を予め確保しておくと良い。

解答2で[出現回数][数値]の2次元配列を作った。

ここに出現回数を計算した結果を格納する

出現回数順に計算結果を取り出す

計算結果を記録しておく手法として動的計画法というのがあるらしい。

NOT EXISTSとDISTINCTでハマった話

大して難しいことではなかったのにハマったので備忘録。

やりたかったこと

投入データでテーブルを更新

投入データに含まれていないけど同じ日付のデータも更新する

下記の例で行くと2023/04/16 group01 02が投入データ。
投入データで更新した後に同日付、2023/04/16 group03と04にだけupdatecheckを入れたい。

更新対象 table2

投入データ table3

・更新SQL

update table2
set
table2.value = t3.value
from table2 t2
left join table3 t3
on t2.tenpo = t3.tenpo
and t2.date = t3.date
and t2.[group] = t3.[group]
where
t3.tenpo IS NOT NULL

・更新後

テーブル結合の結果をイメージできてなかった

最初に書いたSQL

select * from table2 t2
left outer join table3 t3
on t2.tenpo = t3.tenpo
and t2.date = t3.date
and t2.[group] = t3.[group] 
where
t2.tenpo = t3.tenpo
and t2.date = t3.date
and t2.[group] <> t3.[group] 

where句で and t2.[group] <> t3.[group]  しているけどt2とt3で異なるgroupは結合されてないので存在しない。

下記の2023/04/16のグループ03と04を抽出したかった↓

whereなしで結合した結果

DISTINCTを勘違いしていた

そこで次に考えたSQLが下記。

結合条件からグループを消してWhere句で絞り込んでなんとかしたったのだけど行が増殖して困った。

DISTINCT table2.* にするだけでOK。

しかしDISTINCTは1列にしか効かないと思い込んでいたため増殖する時点でアカンと思ってしまった。

select * from table2 t2
left outer join table3 t3
on t2.tenpo = t3.tenpo
and t2.date = t3.date
--where
--なにかの条件

EXISTSを使い慣れていなかった

「ditinct 複数列」とかで検索して行増殖はなんとかなったけど最後に立ちはだかったのはEXISTS。

そもそもEXISTSを使い慣れていなかったので思い浮かばなかった。

まず書いたのはこれ。

結合条件にグループを入れてないから異なるグループとも結合されている。

なのでEXISTSで t2.[group] <> t3.[group]  しても意味無し。

select distinct t2.* from table2 t2
left outer join table3 t3
on t2.tenpo = t3.tenpo
and t2.date = t3.date
where
t3.tenpo IS NOT NULL
and
EXISTS (
select * from table3
where
t2.tenpo = t3.tenpo
and t2.date = t3.date
and t2.[group] <> t3.[group] 
)

正解

下記をUPDATE SELECT文に組み込めばOKだった。

select distinct t2.* from table2 t2
left outer join table3 t3
on t2.tenpo = t3.tenpo
and t2.date = t3.date
where
t3.tenpo IS NOT NULL
and
NOT EXISTS (
select * from table3 t4
where
t2.tenpo = t4.tenpo
and t2.date = t4.date
and t2.[group] = t4.[group] 
)

感想

納期が迫っていて焦ってしまい、だいぶ遠回りをしてしまった。

あとはお勉強だけじゃなくて実践も大切だなあという感想。

もっとバリバリSQLもコードも書きたい。

LeetCodeその3:TwoSum

自分の解答

int配列の中から2つを取り出し、足した値がTargetと同じになる組み合わせを探す問題。

2重ループ。Accept

    public int[] twoSum(int[] nums, int target) {
        int[] answer = new int[2];
        
        for(int i = 0; i < nums.length; i++) {
            for(int j = i + 1; j < nums.length; j++) {
                if(nums[i] + nums[j] == target) {
                    answer[0] = i;
                    answer[1] = j;
                    
                    return  answer;
                }
            }
        }
        return null;
    }

自分の解答2

HashMap使おうとしたが駄目だったパターン。
求める答えを先に出しておく。

   // HashMapバージョン
    /** 結果はWrong。
     * Target = 6; nums = {3, 1, 3}の場合
     * HashMapに重複するキーを登録してしまうため
     * 
     * あとTarget = 4 nums{2, 1, 3}もだめ。
     * 答え[0, 0] を返す可能性があるため。本来は[1, 2]
     *  **/
    public int[] twoSum2(int[] nums, int target) {
        int[] answerArray = new int[2];
        Map<Integer, Integer> map = new HashMap<>();
        
        // 配列をHashMapに格納
        for(int i = 0; i < nums.length; i++) {
            map.put(nums[i], i); //←同じKeyだと上書きされてしまう
        }
        
        // 1重ループで答え探し
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            int answer = target - entry.getKey();
            
            answerArray[0] = map.get(answer);
            answerArray[1] = entry.getValue();
            
            return answerArray;
        }
        return null;
    }

模範解答

重複を避けるテクニックがニクいね!

    // 模範解答
    public int[] twoSum3(int[] nums, int target) {
        HashMap<Integer, Integer> prevMap = new HashMap<>();

        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            int diff = target - num;

            if (prevMap.containsKey(diff)) {
                return new int[] { prevMap.get(diff), i };
            }

            prevMap.put(num, i);// ←あとから追加するので重複を避けられる
        }

        return new int[] {};
    }

・引用元

github.com

学んだこと

以前に何かで読んだ気がするのだけど、求める答えがわかっている場合は先に出力しておくと良いっぽい。

今回の場合は answer = Target - num[i] のところ。

LeetCodeその2:ValidAnagram

自分の解答1

2つの文字がアナグラムになっているかを判定する問題。

Accept

   public boolean isAnagram(String s, String t) {

        // 文字数が違う場合はfalse
        if (s.length() != t.length()) {
            return false;
        }

        Map<Character, Integer> map = new HashMap<>();
        Map<Character, Integer> map2 = new HashMap<>();

        // 文字をKey 数をValueとしてMapに格納
        for (int i = 0; i < s.length(); i++) {
            char char1 = s.charAt(i);

            // 既に格納済みの文字ならスキップ
            if (map.containsKey(char1)) {
                continue;
            }

            int count = 1;
            for (int j = i + 1; j < s.length(); j++) {
                if (char1 == s.charAt(j)) {
                    count++;
                }
            }
            map.put(char1, count);
        }

        // 同上
        for (int i = 0; i < t.length(); i++) {
            char char1 = t.charAt(i);

            // 既に格納済みの文字ならスキップ
            if (map2.containsKey(char1)) {
                continue;
            }

            int count = 1;
            for (int j = i + 1; j < t.length(); j++) {
                if (char1 == t.charAt(j)) {
                    count++;
                }
            }
            map2.put(char1, count);
        }


        // アナグラム判定
        for (Map.Entry<Character, Integer> entry : map.entrySet()) {

            // 同じ単語
            if(map2.containsKey(entry.getKey())) {
                
                // 同じ単語で違う個数
                if(entry.getValue().equals(map2.get(entry.getKey())) == false) {
                    return false;
                }
                
            // 同じ単語が1個もない
            } else {
                return false;
            }
        }
        return true;
    }

自分の解答2

ちょっとコードをきれいにした。

Accept

   // 自分の解答2
    public boolean isAnagram2(String s, String t) {

        // 文字数が違う場合はfalse
        if (s.length() != t.length()) {
            return false;
        }

        Map<Character, Integer> map = new HashMap<>();
        Map<Character, Integer> map2 = new HashMap<>();

        // 文字をKey 数をValueとしてMapに格納
        for (int i = 0; i < s.length(); i++) {
            char char1 = s.charAt(i);

            // 既に格納済みの文字ならスキップ
            if (map.containsKey(char1)) {
                continue;
            }

            int count = 1;
            for (int j = i + 1; j < s.length(); j++) {
                if (char1 == s.charAt(j)) {
                    count++;
                }
            }
            map.put(char1, count);
        }

        // 同上
        for (int i = 0; i < t.length(); i++) {
            char char1 = t.charAt(i);

            // 既に格納済みの文字ならスキップ
            if (map2.containsKey(char1)) {
                continue;
            }

            int count = 1;
            for (int j = i + 1; j < t.length(); j++) {
                if (char1 == t.charAt(j)) {
                    count++;
                }
            }
            map2.put(char1, count);
        }


        // アナグラム判定
        for (Map.Entry<Character, Integer> entry : map.entrySet()) {

            // 同じ単語
            if(map2.containsKey(entry.getKey())) {//containsKeyにして2重ループをやめた。ちょっとスッキリ。
                
                // 同じ単語で違う個数
                if(entry.getValue().equals(map2.get(entry.getKey())) == false) {
                    return false;
                }
                
            // 同じ単語が1個もない
            } else {
                return false;
            }
        }
        return true;
    }

模範解答

理解するのに時間かかった。

コメントで解説。

class Solution {

    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) return false;

        int[] store = new int[26];//a~zの数が26文字なので26要素分の配列を用意

                    /** 例えば s t の1文字目が両方「a」の場合
                    * aを数値にすると97になるので
                    * store[97 - 97] = store[0]++
                    * store[97 - 97] = store[0]-- 
                    * = store[0] = 0
                    * 
                    * もし1文字目 s が「a」 tが「b」の場合は
                    * store[97 - 97] = store[0]++
                    * store[98 - 97] = store[0]-- 
                    *   store[0] = 1
                    *   store[1] = -1
                    * になる。
                    * 
                    * なのでstore配列の要素が全て0ならTRUE
                    * store[0~25] = 0 ならTRUE
                    * 
                    *  **/
        for (int i = 0; i < s.length(); i++) {
            store[s.charAt(i) - 'a']++;
            store[t.charAt(i) - 'a']--;
        }

        for (int n : store) if (n != 0) return false;

        return true;
    }
}

・引用元
leetcode/0242-valid-anagram.java at main · neetcode-gh/leetcode · GitHub

ちょっとずるいやつ

動画解説ではコーディング面接では通らないだろうと言ってた。

面接官は自分で考えたソートアルゴリズムを見たいだろうから。

仕事ならこれが良い。

   public boolean isAnagram(String s, String t) {

        char[] charArray1 = s.toCharArray();
        char[] charArray2 = t.toCharArray();
        
        Arrays.sort(charArray1);
        Arrays.sort(charArray2);
        
        return Arrays.equals(charArray1, charArray2);
    }

HashMapについてのメモ

HashMapはキーと値のセットを要素として持つデータ構造。

参考

わかりやすいのでこっち見たほうが早い↓
JavaにおけるHashMapの仕組みを深堀り - Qiita

検索がO(1)になる理由

HashMapは要素を格納するメモリアドレスの決定方法が特殊。

キーのハッシュ値を要素数で割った余りをアドレスとする。

キーのハッシュ値 % 要素数 = メモリアドレス

ハッシュ値とは

ハッシュ関数 (ハッシュかんすう、英: hash function) あるいは要約関数[1]とは、任意のデータから、別の(多くの場合は短い固定長の)値を得るための操作、または、その様な値を得るための関数のこと。ハッシュ関数から得られた値のことを要約値やハッシュ値または単にハッシュという。

ハッシュ関数は、主に検索の高速化やデータ比較処理の高速化、さらには改竄の検出に使われる。例えば、データベース内の項目を探したり、大きなファイル内で重複しているレコードや似ているレコードを検出したり、核酸の並びから類似する配列を探したりといった場合に利用できる。

・引用元
ハッシュ関数 - Wikipedia

アドレス決定の例

例:要素数3 key:suzuki value:20

suzukiのハッシュ値125(仮)を算出する。

125(ハッシュ値) ÷ 3(要素数) の余り2を取得する。

メモリアドレス2に key:suzuki value:20を格納する。

という感じ。

検索するときも同様にKeyからアドレスを特定できるのでO(1)で検索可能!
よく考えるよなあこんなこと。